iT邦幫忙

2026 iThome 鐵人賽

DAY 17
1
Software Development

30天打造一套企業PLM系列 第 17

Day 17:前端架構——Zustand 切片與前端快取

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260903/20161290Vq9HAxFLoa.png

系列:30 天打造企業級 PLM|面向:前端|素材:前端狀態切片、React Query、MenuService

問題場景

企業系統前端的狀態爆炸來得很快:登入者與權限、當前表單、當前品項、metadata 快取、Modal 開關、主題、語系、搜尋結果追蹤。全塞一個 store 是災難的開始,散落各元件的 useState 是另一種災難。今天講 Mini-PLM 前端的核心:Zustand 切片與 server-state cache。

商業邏輯設計

  • 選單即權限。使用者看得到哪些入口由角色決定(Day 7 的 mp_role_folder),但選單只負責導覽與呈現,不能成為前端授權來源
  • 功能鍵按 Privilege 渲染,這是 Day 6 簡化決策在前端的另一半:前端隱藏是體驗,後端守門是安全,缺一不可

技術選型與取捨

架構演進:從 JSP 整頁刷新 MPA 到現代 React SPA 狀態切片

以前 Oracle Agile PLM 的 Web Client 是典型的 Multi-Page Application (MPA)

在那個 Struts + JSP 的年代:

  1. 無前端狀態管理:前端幾乎沒有「應用程式狀態」的概念。每次點擊選單、切換 Tab 或搜尋,都是一次完整的 HTTP Request / Response,由伺服器重跑一次業務邏輯並拼裝整頁 HTML 吐回,操作體驗極為頓挫。
  2. 跨頁面狀態靠 Session 苦撐:如果要在多個操作步驟間共享上下文(例如當前編輯的料號 ID、暫存清單),只能死死依賴伺服器端的 HttpSession,在多視窗或多 Tab 操作時極易互相踩踏覆蓋。

Mini-PLM 採用現代 React 19 SPA 架構,透過 Zustand 進行細緻的領域狀態切片,將互動與狀態響應收斂在前端瀏覽器中。

為何選 Zustand 不用 Redux

三個理由。樣板代碼量:一個 store 十幾行,沒有 action type 與 dispatcher 的儀式。學習曲線:新人半天上手。選擇性訂閱天然內建:useThemeStore((s) => s.color) 只在 color 變時重渲染。放棄的是 Redux DevTools 的時間旅行與嚴格的單向資料流紀律,對這個團隊規模,換得起。

伺服器狀態另外走 TanStack Query。API 資料的快取、重抓、失效是 Query 的事,Zustand 只放純前端狀態。兩者的界線就一句:這個狀態有沒有伺服器上的真相來源?有就 Query,沒有就 Zustand。

https://ithelp.ithome.com.tw/upload/images/20260903/20161290lnhPTy0zzA.png

十個 store 的切片策略:按領域,不按頁面

Store 職責
useAuthStore JWT、登入者、權限
useFormStore / useItemStore 當前表單/品項的操作上下文
useMetaStore Form/Item Type 與欄位定義(全域快取,Day 8 的 meta 來源)
useRefDataStore 參考資料快取(動態列表、外部 lookup)
useUIStore / useThemeStore / useLocaleStore Modal 開關/主題色/語系
useResultTrackStore / useSearchCacheStore 搜尋結果追蹤/條件快取

按領域切而不是按頁面切,因為頁面會重組(Modal 變頁、頁拆成兩頁),領域不會。切片的判斷句是「這個狀態屬於哪個業務概念」,不是「這個狀態被哪個頁面用」。store 之間依賴保持單向,業務 store 可讀 authStore,反向禁止,避免初始化順序地獄。

選單仍由 mp_menu 提供導覽資料,後端 MenuService 使用 Spring Cache;本文聚焦前端如何管理這些 server state,不展開路由實作。

動態選單還有一個部署面的配套:啟動時自我補齊。系統內建的管理頁(Manager Import、Config Migration 這類)若要求每個環境部署後都手動去 ConfigMenu 建一列,遲早漏。AdminMenuBootstrapServiceApplicationReadyEvent 時依 link upsert 這些固定入口:

@EventListener(ApplicationReadyEvent.class)
@Transactional   // 交易必須放在事件入口方法,避免 self-invocation 使 @Transactional 失效
@CacheEvict(value = { "menuCache", "userCache" }, allEntries = true)
public void initAfterApplicationReady() {
    init();
}

「使用者自建的選單走 ConfigMenu、系統內建的入口走啟動 upsert」,兩條路徑各管各的,環境重建不再需要一張手動建選單的 checklist。

前端值得快取什麼

優先快取「讀多寫少、可由事件失效」的 server state:Form/Item Type 與欄位 metadata、使用者選單與 privilege、參考資料(使用者、群組、workflow)、最近瀏覽與 bookmarks。這些資料統一放 TanStack Query,以 query key 區分使用者、物件 ID、分頁、排序與搜尋條件;mutation 或 SSE 事件到達後精準 invalidateQueries

不建議把 JWT、表單草稿、權限結果或 Query cache 直接持久化到 localStorage:前兩者有安全與跨帳號殘留風險,後兩者容易展示過期資料。登出時清空 QueryClient;跨分頁若要同步,只同步「失效通知」,不要同步敏感 payload。

https://ithelp.ithome.com.tw/upload/images/20260903/20161290gie3RATdsH.png

踩坑記錄

  • 登出後的殘留狀態:切帳號登入,前一個帳號的搜尋快取、結果追蹤還在 store 裡。store 一旦有了屬於某個使用者的資料,就要在 logout 時統一 reset。後來的紀律是每個 store 附 reset(),logout 流程逐一呼叫
  • 選單更新使用者看不到:前端 query cache 必須接上 SSE 的 menu/permission update 事件;登出時也要清除 QueryClient,避免切帳號命中前一個使用者的資料
  • 選單 bootstrap 讓後端啟動即崩潰:AdminMenuBootstrapService 早期用 findByLink 查既有選單,某環境歷史資料同一個 link 有兩列,非唯一結果直接拋例外——啟動流程掛在選單補齊上,整個後端起不來。修法是改成清單查詢容忍重複:保留第一筆更新、其餘停用並留 log,bootstrap 從「遇髒資料就死」變成「順手自我修復」。啟動期程式碼的容錯標準要比執行期更嚴,因為它一失敗就是全系統陪葬

小結

狀態按領域切片,伺服器狀態歸 Query、純前端歸 Zustand;cache 以 query key、事件失效與登出清除維持正確性。明日 Day 18:antd/ProTable 實戰陷阱與 i18n。


上一篇
Day 16:進階搜尋——條件建構器+動態 Predicate
系列文
30天打造一套企業PLM17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言